﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using SharpShare.Storage;
using System.Numerics;

namespace SharpShare.Afp.Protocol {
    public sealed class AfpStream {
        public static implicit operator AfpStream(Stream stream) {
            return new AfpStream(stream);
        }

        private Dictionary<string, long> _markedOffsets = new Dictionary<string, long>();
        private Dictionary<string, long> _readMarkOffsets = new Dictionary<string, long>();
        private Stack<long> _markBeginnings = new Stack<long>();

        public AfpStream()
            : this(new MemoryStream()) {

        }
        public AfpStream(byte[] data)
            : this(new MemoryStream(data)) {

        }
        public AfpStream(Stream stream) {
            this.Stream = stream;
            _markBeginnings.Push(0);
        }

        public Stream Stream { get; private set; }

        public bool EOF { get { return (this.Stream.Position == this.Stream.Length); } }

        public void BeginMarking() {
            _markBeginnings.Push(this.Stream.Position);
        }
        public void EndMarking() {
            _markBeginnings.Pop();
        }

        #region Reading

        public BigInteger ReadBigInteger(int size) {
            byte[] data = this.ReadBytes((uint)size);
            BigInteger i = data.FromAfpNetworkByteArray();

            return i;
        }
        public void ReadMark(string name) {
            if (_readMarkOffsets.ContainsKey(name)) {
                throw new ArgumentException("A mark with this name has already been read.", "name");
            }

            ushort offset = this.ReadUInt16();
            _readMarkOffsets[name] = offset;
        }
        public void BeginReadingMark(string name) {
            if (!_readMarkOffsets.ContainsKey(name)) {
                throw new ArgumentException("A mark with this name was never read.", "name");
            }

            long markStartPosition = _markBeginnings.Peek();
            long offset = _readMarkOffsets[name];
            _readMarkOffsets.Remove(name);

            long dataStart = (markStartPosition + offset);
            this.Stream.Position = dataStart;
        }
        public string ReadUTF8StringWithHint() {
            uint hint = this.ReadUInt32();
            return this.ReadUTF8String();
        }
        public string ReadUTF8String() {
            ushort length = this.ReadUInt16();
            byte[] data = this.ReadBytes(length);
            return System.Text.Encoding.UTF8.GetString(data);
        }
        public string ReadPascalString() {
            byte length = this.ReadUInt8();
            byte[] data = this.ReadBytes(length);

            return System.Text.Encoding.ASCII.GetString(data);
        }

        public TEnum ReadEnum<TEnum>() where TEnum : struct {
            Type enumType = Enum.GetUnderlyingType(typeof(TEnum));
            long number = this.ReadNumber(enumType);

            object enumValue = Enum.ToObject(typeof(TEnum), number);
            return (TEnum)enumValue;
        }

        public long ReadNumber(Type type) {
            if (type == typeof(byte)) {
                return (long)this.ReadUnsignedNumber(1);
            }
            if (type == typeof(ushort)) {
                return (long)this.ReadUnsignedNumber(2);
            }
            if (type == typeof(uint)) {
                return (long)this.ReadUnsignedNumber(4);
            }

            if (type == typeof(sbyte)) {
                return (long)this.ReadSignedNumber(1);
            }
            if (type == typeof(short)) {
                return (long)this.ReadSignedNumber(2);
            }
            if (type == typeof(int)) {
                return (long)this.ReadSignedNumber(4);
            }

            throw new Exception("Unrecognized type.");
        }
        public ulong ReadUnsignedNumber(uint length) {
            byte[] data = this.ReadBytesAndFlip(length);

            switch (length) {
                case 1:
                    return data[0];
                case 2:
                    return BitConverter.ToUInt16(data, 0);
                case 4:
                    return BitConverter.ToUInt32(data, 0);
                case 8:
                    return BitConverter.ToUInt64(data, 0);
                default:
                    throw new Exception("Invalid length.");
            }
        }
        public long ReadSignedNumber(uint length) {
            byte[] data = this.ReadBytesAndFlip(length);

            switch (length) {
                case 1:
                    return data[0];
                case 2:
                    return BitConverter.ToInt16(data, 0);
                case 4:
                    return BitConverter.ToInt32(data, 0);
                case 8:
                    return BitConverter.ToInt64(data, 0);
                default:
                    throw new Exception("Invalid length.");
            }
        }


        public long ReadInt64() {
            return (long)this.ReadSignedNumber(8);
        }
        public int ReadInt32() {
            return (int)this.ReadSignedNumber(4);
        }
        public short ReadInt16() {
            return (short)this.ReadSignedNumber(2);
        }
        public sbyte ReadInt8() {
            return (sbyte)this.ReadSignedNumber(1);
        }

        public ulong ReadUInt64() {
            return (ulong)this.ReadUnsignedNumber(8);
        }
        public uint ReadUInt32() {
            return (uint)this.ReadUnsignedNumber(4);
        }
        public ushort ReadUInt16() {
            return (ushort)this.ReadUnsignedNumber(2);
        }
        public byte ReadUInt8() {
            return (byte)this.ReadUnsignedNumber(1);
        }

        public byte[] ReadBytes(uint count) {
            byte[] bytes = new byte[count];
            int rec = this.Stream.Read(bytes, 0, (int)count);

            if (rec < count) {
                throw new Exception("Reached end of stream before reading requested bytes.");
            }

            return bytes;
        }

        public byte[] ReadBytesAndFlip(uint count) {
            byte[] bytes = this.ReadBytes(count);
            Array.Reverse(bytes);
            return bytes;
        }
        public DateTime ReadMacintoshDate() {
            uint macDate = this.ReadUInt32();
            DateTime date = macDate.FromMacintoshDate();
            return date;
        }

        public AfpUnixPrivileges ReadUnixPrivileges() {
            AfpUnixPrivileges p = new AfpUnixPrivileges() {
                uid = this.ReadUInt32(),
                gid = this.ReadUInt32(),
                permissions = this.ReadEnum<AfpUnixPrivilegePermissions>(),
                accessRights = this.ReadEnum<AfpAccessRightsBitmap>()
            };
            return p;
        }
        public IEnumerable<string> ReadPath() {
            AfpPathType type = this.ReadEnum<AfpPathType>();
            return this.ReadPath(type);
        }
        public IEnumerable<string> ReadPath(AfpPathType pathType) {
            string path;

            switch (pathType) {
                case AfpPathType.kFPLongName:
                case AfpPathType.kFPShortName:
                    path = this.ReadPascalString();
                    break;
                case AfpPathType.kFPUTF8Name:
                    path = this.ReadUTF8StringWithHint();
                    break;
                default:
                    throw new Exception();
            }

            if (path.Length == 0) {
                return Enumerable.Empty<string>();
            }

            if (path[0] == '\0') {
                path = path.Remove(0, 1);
            }

            string[] splitPath = path.Split('\0');

            return splitPath;
        }

        public IStorageItem ReadPathAndFind(IAfpSession session, ushort volumeId, uint directoryId, out IAfpVolume volume) {
            var pathComponents = this.ReadPath();
            return session.FindPath(pathComponents, volumeId, directoryId, out volume);
        }

        #endregion

        #region Writing

        public void WriteBigInteger(BigInteger i, int size = -1) {
            byte[] data = i.ToAfpNetworkByteArray(size);
            this.WriteBytes(data);
        }

        public void WriteUnixPrivileges(AfpUnixPrivileges priv) {
            this.WriteUInt32(priv.uid);
            this.WriteUInt32(priv.gid);
            this.WriteEnum(priv.permissions);
            this.WriteEnum(priv.accessRights);
        }

        public void WriteMacintoshDate(DateTime date) {
            if (date == DateTime.MinValue) {
                this.WriteUInt32(0x80000000);
                return;
            }

            uint macDate = date.ToMacintoshDate();
            this.WriteUInt32(macDate);
        }

        public void WriteUTF8StringWithHint(string text) {
            this.WriteUInt32(0);
            this.WriteUTF8String(text);
        }
        public void WriteUTF8String(string text) {

            byte[] data = System.Text.UTF8Encoding.UTF8.GetBytes(text);
            this.WriteUInt16((ushort)data.Length);
            this.WriteBytes(data);
        }
        public void WritePascalString(string text) {
            this.WritePascalString(text, 255);
        }
        public void WritePascalString(string text, int limit) {
            limit = System.Math.Min(limit, 255);

            if (text.Length > limit) {
                text = text.Substring(0, limit);
            }

            this.WriteUInt8((byte)text.Length);

            byte[] ascii = System.Text.Encoding.ASCII.GetBytes(text);
            this.WriteBytes(ascii);
        }
        public void WriteMark(string name) {
            if (!this.Stream.CanSeek) {
                throw new ArgumentException("Base stream is not seekable.");
            }

            if (_markedOffsets.ContainsKey(name)) {
                throw new ArgumentException("Already marked with this name.");
            }

            _markedOffsets[name] = this.Stream.Position;

            this.WriteUInt16(0);
        }
        public void BeginMark(string name) {
            if (!_markedOffsets.ContainsKey(name)) {
                throw new ArgumentException("Marked name doesn't exist.");
            }

            long offset = _markedOffsets[name];
            _markedOffsets.Remove(name);

            long currentOffset = this.Stream.Position;
            this.Stream.Position = offset;

            this.WriteUInt16((ushort)(currentOffset - _markBeginnings.Peek()));
            this.Stream.Position = currentOffset;
        }

        public void WriteEnum<TEnum>(TEnum value) where TEnum : struct {
            Type enumType = Enum.GetUnderlyingType(typeof(TEnum));
            long number = Convert.ToInt64(value);

            if (enumType == typeof(byte)) {
                this.WriteUnsignedNumber((ulong)number, 1);
            } else if (enumType == typeof(ushort)) {
                this.WriteUnsignedNumber((ulong)number, 2);
            } else if (enumType == typeof(uint)) {
                this.WriteUnsignedNumber((ulong)number, 4);
            } else if (enumType == typeof(sbyte)) {
                this.WriteSignedNumber(number, 1);
            } else if (enumType == typeof(short)) {
                this.WriteSignedNumber(number, 2);
            } else if (enumType == typeof(int)) {
                this.WriteSignedNumber(number, 4);
            } else {
                throw new Exception("Invalid enum type.");
            }
        }
        public void WriteUnsignedNumber(ulong number, int length) {
            switch (length) {
                case 1:
                    this.WriteBytes(new byte[] { (byte)number });
                    break;
                case 2:
                    this.FlipAndWriteBytes(BitConverter.GetBytes((ushort)number));
                    break;
                case 4:
                    this.FlipAndWriteBytes(BitConverter.GetBytes((uint)number));
                    break;
                case 8:
                    this.FlipAndWriteBytes(BitConverter.GetBytes((ulong)number));
                    break;
                default:
                    throw new Exception("Invalid length.");
            }
        }
        public void WriteSignedNumber(long number, int length) {
            switch (length) {
                case 1:
                    this.WriteBytes(new byte[] { (byte)number });
                    break;
                case 2:
                    this.FlipAndWriteBytes(BitConverter.GetBytes((short)number));
                    break;
                case 4:
                    this.FlipAndWriteBytes(BitConverter.GetBytes((int)number));
                    break;
                case 8:
                    this.FlipAndWriteBytes(BitConverter.GetBytes((long)number));
                    break;
                default:
                    throw new Exception("Invalid length.");
            }
        }

        public void WriteInt64(long value) {
            this.WriteSignedNumber(value, 8);
        }
        public void WriteInt32(int value) {
            this.WriteSignedNumber(value, 4);
        }
        public void WriteInt16(short value) {
            this.WriteSignedNumber(value, 2);
        }

        public void WriteUInt8(byte value) {
            this.WriteUnsignedNumber(value, 1);
        }
        public void WriteUInt16(ushort value) {
            this.WriteUnsignedNumber(value, 2);
        }
        public void WriteUInt32(uint value) {
            this.WriteUnsignedNumber(value, 4);
        }
        public void WriteUInt64(ulong value) {
            this.WriteUnsignedNumber(value, 8);
        }

        public void WriteBytes(byte[] bytes, int offset, int length) {
            this.Stream.Write(bytes, offset, length);
        }
        public void WriteBytes(byte[] bytes) {
            this.WriteBytes(bytes, 0, bytes.Length);
        }
        public void FlipAndWriteBytes(byte[] bytes) {
            Array.Reverse(bytes);
            this.WriteBytes(bytes);
        }

        public uint ReadPadding(uint alignment = 2) {
            if (this.EOF) {
                return 0;
            }

            uint padBytes = (uint)(this.Stream.Position % alignment);
            this.ReadBytes((uint)padBytes);
            return padBytes;
        }
        public uint WritePadding(uint alignment = 2) {
            uint padBytes = ((uint)this.Stream.Length % alignment);
            this.WriteBytes(new byte[padBytes]);
            return padBytes;
        }

        #endregion

        public byte[] ToByteArray() {
            MemoryStream memStream = (this.Stream as MemoryStream);
            if (memStream == null) {
                throw new InvalidOperationException("Base stream is not a memory stream.");
            }

            return memStream.ToArray();
        }
    }

    public static class AfpStreamExtensions {
        public static byte[] ToAfpNetworkByteArray(this BigInteger i, int size = -1) {
            if (i.Sign == -1) {
                throw new ArgumentException("This doesn't work with negative numbers.", "i");
            }

            byte[] data = i.ToByteArray();
            if (size == -1) {
                size = data.Length;
            }

            // Remove sign byte if necessary.
            if (data[data.Length - 1] == 0 && (data[data.Length - 2] & 0x80) == 0x80) {
                Array.Resize(ref data, data.Length - 1);
            }

            Array.Resize(ref data, size);
            Array.Reverse(data);

            return data;
        }

        public static BigInteger FromAfpNetworkByteArray(this byte[] data) {
            byte[] useData = new byte[data.Length];
            Array.Copy(data, useData, useData.Length);

            Array.Reverse(useData);

            if ((useData[useData.Length - 1] & 0x80) == 0x80) {
                Array.Resize(ref useData, useData.Length + 1);
            }

            BigInteger i = new BigInteger(useData);

            return i;
        }
    }
}
